package com.naiterui.ehp.bs.doctor.utils;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.base.Charsets;
import com.google.common.collect.Lists;
import com.google.common.hash.Hashing;
import com.naiterui.common.redis.RedisUtil;
import com.naiterui.ehp.bp.constants.CommonConstant;
import com.naiterui.ehp.bp.support.exception.BusinessException;
import com.naiterui.ehp.bp.support.exception.JsonException;
import com.naiterui.ehp.bp.support.utils.JsonMapper;
import com.naiterui.ehp.bp.utils.biz.ScheduleUtils;
import com.naiterui.ehp.bp.utils.date.DateUtil;
import com.naiterui.ehp.bp.utils.lang.ConfigUtil;
import com.naiterui.ehp.bp.utils.net.RestHttpUtil;
import com.naiterui.ehp.bs.doctor.exception.ExceptionCodes;
import com.naiterui.ehp.bs.doctor.vo.TencentCloudAccessTokenVO;
import com.naiterui.ehp.bs.doctor.vo.TencentFaceIdVO;
import lombok.Builder;
import lombok.Data;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.ResponseEntity;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

/**
 * @author guoyongxiang
 * @date 2020/4/29 19:42
 * @since 1.0.0
 */
public class AdTencentCloudSdkUtil {

    private static final Logger LOGGER = LoggerFactory.getLogger(AdTencentCloudSdkUtil.class);

    private static final String ACCESS_TOKEN_URL = "https://idasc.webank.com/api/oauth2/access_token";

    private static final String API_TICKET_URL = "https://idasc.webank.com/api/oauth2/api_ticket";

    private static final String FACE_ID_URL = "https://idasc.webank.com/api/server/getfaceid";

    private static final String FACE_SYNC_URL = "https://idasc.webank.com/api/server/sync";

    private static final String GRANT_TYPE_CLIENT_CREDENTIAL = "client_credential";

    private static final String VERSION = "1.0.0";

    private static final int ACCESS_TOKEN_REFRESH_UNIT = 20;
    public static final int ACCESS_TOKEN_CACHE_TIME = (int) TimeUnit.MINUTES.toSeconds(ACCESS_TOKEN_REFRESH_UNIT + 5);


    private static final String ACCESSTOKEN_KEY_PRE = CommonConstant.REDIS_PRE_DR + "TENCENT_ACCESSTOKEN_";
    private static final String SIGN_TICKET_PRE = CommonConstant.REDIS_PRE_DR + "TENCENT_SIGN_";
    private static final String ACCESSTOKEN_REFRESH_LOCK_PRE = CommonConstant.REDIS_PRE_DR + "TENCENT_ACCESSTOKEN_LOCK";

    private static final String H5_FACE_ID_URL = "https://miniprogram-kyc.tencentcloudapi.com/api/server/h5/geth5faceid";
    private static final String H5_FACE_LOGIN_URL = "/api/web/login";
    private static final String PC_FACE_LOGIN_URL = "/api/pc/login";
    private static final String H5_FACE_SYNC_URL = "https://miniprogram-kyc.tencentcloudapi.com/api/server/sync";

    private static String TASK_SCHEDULE_HOST = ConfigUtil.getString("schedule.host.url");
    private static String ACCESS_TOKEN_REFRESH_URL = ConfigUtil.getString("tencent-cloud.access.token.refresh");

    private static String wbAppId = ConfigUtil.getString("tencent-cloud.ad.wbAppId");
    private static String wbAppKey = ConfigUtil.getString("tencent-cloud.ad.wbAppKey");

    public static final String CODE_SUCCESS = "0";

    public static TencentFaceIdVO getFaceid(String orderNo, String name, String idNo, String userId) throws BusinessException {
        Map<String, String> paramMap = new HashMap<>();
        paramMap.put("webankAppId", wbAppId);
        paramMap.put("orderNo", orderNo);
        paramMap.put("name", name);
        paramMap.put("idNo", idNo);
        paramMap.put("userId", userId);
        paramMap.put("version", VERSION);
        String nonceStr = RandomStringUtils.random(32, true, true);
        paramMap.put("nonce", nonceStr);

        String ticket = getSignTicket();
        String sign = sign(Lists.newArrayList(wbAppId, userId, nonceStr, VERSION), ticket);
        paramMap.put("sign", sign);

        String json = JSON.toJSONString(paramMap);
        LOGGER.info("腾讯云人脸-请求参数 orderNo:{}, userId:{}, nonceStr:{}, ticket:{}, body:{}", orderNo, userId, nonceStr, ticket, json);
        ResponseEntity<String> responseEntity = RestHttpUtil.post()
            .url(FACE_ID_URL).body(json)
            .execute(new ParameterizedTypeReference<String>() {
            });
        LOGGER.info("腾讯云人脸-出参 orderNo:{}, userId:{}, body:{}", orderNo, userId, responseEntity.getBody());

        JSONObject jsonObject = JSON.parseObject(responseEntity.getBody());
        Boolean success = jsonObject.getJSONObject("result").getBoolean("success");
        String code = jsonObject.getString("code");
        if (!"0".equals(code)) {
            String msg = jsonObject.getString("msg");
            LOGGER.warn("腾讯云人脸-请求失败 orderNo:{}, userId:{}, code:{}, message:{}", orderNo, userId, code, msg);
            throw new BusinessException(ExceptionCodes.EXT_TENCEN_CLOUD_ERR, "(" + code + ")" + msg);
        }
        String bizSeqNo = jsonObject.get("bizSeqNo").toString();
        String faceId = jsonObject.getJSONObject("result").get("faceId").toString();
        orderNo = jsonObject.getJSONObject("result").get("orderNo").toString();
        String ticketNonce = getApiTicket(userId, "NONCE");
        TencentFaceIdVO faceIdVO = TencentFaceIdVO.builder()
            .bizSeqNo(bizSeqNo)
            .agreementNo(orderNo)
            .faceId(faceId)
            .openApiNonce(nonceStr)
            .openApiUserId(userId)
            .openApiSign(sign(Lists.newArrayList(wbAppId, userId, nonceStr, VERSION), ticketNonce))
            .build();
        LOGGER.info("腾讯云人脸-响应客户端参数 orderNo:{}, userId:{}, body:{}", orderNo, userId, JSON.toJSON(faceIdVO));
        return faceIdVO;
    }

    public static FaceSyncVO getFaceidSync(String orderNo, String userId) throws BusinessException {
        Map<String, String> paramMap = new HashMap<>();
        paramMap.put("app_id", wbAppId);
        paramMap.put("order_no", orderNo);
        paramMap.put("version", VERSION);

        String nonceStr = RandomStringUtils.random(32, true, true);
        paramMap.put("nonce", nonceStr);

        String ticket = getSignTicket();
        String sign = sign(Lists.newArrayList(wbAppId, orderNo, nonceStr, VERSION), ticket);
        paramMap.put("sign", sign);

        LOGGER.info("获取人脸核身结果，请求参数 orderNo:{}, userId:{}, nonce:{}, ticket:{}, sign:{}, paramMap:{}", orderNo, userId, nonceStr, ticket,
            sign, paramMap);
        ResponseEntity<String> responseEntity = RestHttpUtil.get()
            .url(FACE_SYNC_URL)
            .paramMap(paramMap)
            .execute(new ParameterizedTypeReference<String>() {
            });
        LOGGER.info("获取人脸核身结果，响应结果 orderNo:{}, userId:{}, result:{}", orderNo, userId, responseEntity.getBody());
        JSONObject jsonObject = JSON.parseObject(responseEntity.getBody());
        String code = jsonObject.get("code").toString();
        String msg = jsonObject.get("msg").toString();
        FaceSyncVO faceIdVO;
        if ("0".equals(code)) {
            JSONObject resultJsonObject = jsonObject.getJSONObject("result");
            String name = resultJsonObject.getString("name");
            String idNo = resultJsonObject.getString("idNo");
            faceIdVO = FaceSyncVO.builder().result(true).name(name).idNo(idNo).build();
        } else {
            faceIdVO = FaceSyncVO.builder().result(false).build();
            LOGGER.warn("人脸核身校验失败 orderNo:{}, userId:{}, msg:{}", orderNo, userId, msg);
        }
        return faceIdVO;
    }

    public static String refreshAccessToken(String appId, String secret) {
        String accessToken = "";
        Map<String, String> paramMap = new HashMap<>();
        paramMap.put("app_id", appId);
        paramMap.put("secret", secret);
        paramMap.put("grant_type", GRANT_TYPE_CLIENT_CREDENTIAL);
        paramMap.put("version", VERSION);
        ResponseEntity<TencentCloudAccessTokenVO> responseEntity =
            RestHttpUtil.get().url(ACCESS_TOKEN_URL).paramMap(paramMap).execute(new ParameterizedTypeReference<TencentCloudAccessTokenVO>() {
            });
        TencentCloudAccessTokenVO accessTokenResult = responseEntity.getBody();
        accessToken = accessTokenResult.getAccessToken();
        if (StringUtils.isBlank(accessToken)) {
            LOGGER.error("获取tencentCloud AccessToken失败，结果：{}", accessTokenResult);
            return accessToken;
        }
        //新token放入缓存
        // AccessToken有效期2小时,缓存1小时55分，提前失效，微信会保证新旧token5分钟内可共同使用
        RedisUtil.valueOps().setString(accessTokenKey(appId), accessToken, ACCESS_TOKEN_CACHE_TIME);

        // 添加定时任务，1小时51分后刷新缓存，先调任务删除接口，再添加新的调度
        Date refreshTime = DateUtil.addMinutes(new Date(), ACCESS_TOKEN_REFRESH_UNIT);
        //DateUtil.plus(new Date(), ACCESS_TOKEN_REFRESH_UNIT, ChronoUnit.MINUTES);
        String refreshTimeStr = DateUtil.formatDate(refreshTime, DateUtil.CRON_TIME_FORMAT);
        Map<String, String> scheduleParam = new HashMap<>();
        try {
            //先删除未执行的刷新任务
            ScheduleUtils.removeCustomerTask(TASK_SCHEDULE_HOST, ScheduleUtils.TASK_TYPE_ACCESS_TOKEN, appId);
            //添加新的刷新任务
            ScheduleUtils.addCustomerTask(TASK_SCHEDULE_HOST, refreshTimeStr,
                ScheduleUtils.TASK_TYPE_ACCESS_TOKEN, appId, ACCESS_TOKEN_REFRESH_URL, scheduleParam);
        } catch (Exception e) {
            LOGGER.error("tencentCloud accessToken刷新任务添加失败，errmsg={}", e);
        }

        // 刷新sign ticket
        try {
            String apiTicket = getApiTicket(null, "SIGN");
            RedisUtil.valueOps().setString(signTicketKey(appId), apiTicket, ACCESS_TOKEN_CACHE_TIME + 1);
        } catch (BusinessException e) {
            LOGGER.warn("刷新 sign ticket 失败, msg:{}", e.getMessage());
        }

        return accessToken;
    }

    public static String accessToken() {
        String appId = wbAppId;
        String accessToken = Optional.ofNullable(appId)
            .map(s -> accessTokenKey(appId))
            .map(cacheKey -> RedisUtil.valueOps().getString(cacheKey))
            .orElse(null);
        if (null == accessToken) {
            accessToken = refreshAccessToken(appId, wbAppKey);
        }

        return accessToken;
    }

    private static String getSignTicket() {
        String appId = wbAppId;
        String ticket = Optional.ofNullable(appId)
            .map(s -> signTicketKey(appId))
            .map(cacheKey -> RedisUtil.valueOps().getString(cacheKey))
            .orElse(null);
        if (null == ticket) {
            try {
                ticket = getApiTicket(null, "SIGN");
                RedisUtil.valueOps().setString(signTicketKey(appId), ticket, ACCESS_TOKEN_CACHE_TIME + 1);
            } catch (BusinessException e) {
                LOGGER.warn("刷新api ticket 失败 {}", e.getMessage());
            }
        }
        return ticket;
    }

    private static String getApiTicket(String userId, String type) throws BusinessException {
        Map<String, String> paramMap = new HashMap<>();
        paramMap.put("app_id", wbAppId);
        paramMap.put("access_token", accessToken());
        paramMap.put("type", type);
        paramMap.put("version", VERSION);
        if ("NONCE".equals(type)) {
            paramMap.put("user_id", userId);
        }
        ResponseEntity<String> responseEntity =
            RestHttpUtil.get().url(API_TICKET_URL).paramMap(paramMap).execute(new ParameterizedTypeReference<String>() {
            });
        String result = responseEntity.getBody();
        try {
            LOGGER.info("获取 apiTicket {}", result);
            JsonNode node = JsonMapper.toNode(result);
            String code = node.get("code").asText();

            if (!"0".equals(code)) {
                String msg = node.get("msg").asText();
                LOGGER.warn("获取 apiTicket 失败 userId:{}, result:{}", userId, result);
                throw new BusinessException(ExceptionCodes.EXT_TENCEN_CLOUD_ERR, msg);
            }
            JsonNode tickets = node.get("tickets").get(0);
            return tickets.get("value").textValue();
        } catch (JsonException e) {
            LOGGER.error("获取 apiTicket 异常 userId:{}, result:{}", userId, result, e);
            throw new BusinessException(ExceptionCodes.EXT_TENCEN_CLOUD_ERR, "获取腾讯云 apiTicket 异常");
        }
    }

    /**
     * accesstoken 缓存key
     *
     * @param appId
     *
     * @return
     */
    public static String accessTokenKey(String appId) {
        return ACCESSTOKEN_KEY_PRE + appId;
    }

    public static String signTicketKey(String appId) {
        return SIGN_TICKET_PRE + appId;
    }

    public static String sign(List<String> values, String ticket) {
        if (values == null) {
            throw new NullPointerException("values is null");
        }
        values.removeAll(Collections.singleton(null));// remove null
        values.add(ticket);
        java.util.Collections.sort(values);

        StringBuilder sb = new StringBuilder();
        for (String s : values) {
            sb.append(s);
        }
        return Hashing.sha1().hashString(sb, Charsets.UTF_8).toString().toUpperCase();
    }

    /**
     * accesstoken 刷新锁缓存key
     *
     * @param appId
     * @return
     */
    public static String accessTokenLockKey(String appId) {
        return ACCESSTOKEN_REFRESH_LOCK_PRE + appId;
    }

    public static FaceSyncVO getH5FaceidSync(String orderNo, String userId) throws BusinessException {
        Map<String, String> paramMap = new HashMap<>();
        paramMap.put("app_id", wbAppId);
        paramMap.put("order_no", orderNo);
        paramMap.put("version", VERSION);

        String nonceStr = RandomStringUtils.random(32, true, true);
        paramMap.put("nonce", nonceStr);

        String ticket = getSignTicket();
        String sign = sign(Lists.newArrayList(wbAppId, orderNo, nonceStr, VERSION), ticket);
        paramMap.put("sign", sign);

        LOGGER.info("获取人脸核身结果，请求参数 orderNo:{}, userId:{}, nonce:{}, ticket:{}, sign:{}, paramMap:{}", orderNo, userId, nonceStr, ticket,
                sign, paramMap);
        ResponseEntity<String> responseEntity = RestHttpUtil.get()
                .url(H5_FACE_SYNC_URL)
                .paramMap(paramMap)
                .execute(new ParameterizedTypeReference<String>() {
                });
        LOGGER.info("获取人脸核身结果，响应结果 orderNo:{}, userId:{}, result:{}", orderNo, userId, responseEntity.getBody());
        JSONObject jsonObject = JSON.parseObject(responseEntity.getBody());
        String code = jsonObject.get("code").toString();
        String msg = jsonObject.get("msg").toString();
        JSONObject resultJsonObject = jsonObject.getJSONObject("result");
        String name = resultJsonObject.getString("name");
        String idNo = resultJsonObject.getString("idNo");
        FaceSyncVO faceIdVO;
        if (CODE_SUCCESS.equals(code)) {
            faceIdVO = FaceSyncVO.builder().result(true).name(name).idNo(idNo).build();
        } else {
            faceIdVO = FaceSyncVO.builder().result(false).name(name).idNo(idNo).build();
            LOGGER.warn("人脸核身校验失败 orderNo:{}, userId:{}, msg:{}", orderNo, userId, msg);
        }
        return faceIdVO;
    }

    public static TencentFaceIdVO getH5Faceid(String orderNo, String name, String idNo, String userId) throws BusinessException {
        Map<String, String> paramMap = new HashMap<>();
        paramMap.put("webankAppId", wbAppId);
        paramMap.put("orderNo", orderNo);
        paramMap.put("name", name);
        paramMap.put("idNo", idNo);
        paramMap.put("userId", userId);
        paramMap.put("version", VERSION);

        String ticket = getApiTicket(userId, "SIGN");
        String nonceStr = RandomStringUtils.random(32, true, true);
        String sign = sign(Lists.newArrayList(wbAppId, orderNo, name, idNo, userId, VERSION), ticket);
        paramMap.put("sign", sign);

        String json = JSON.toJSONString(paramMap);
        LOGGER.info("腾讯云人脸-请求参数 orderNo:{}, userId:{}, nonceStr:{}, ticket:{}, body:{}", orderNo, userId, nonceStr, ticket, json);
        ResponseEntity<String> responseEntity = RestHttpUtil.post()
                .url(H5_FACE_ID_URL).body(json)
                .execute(new ParameterizedTypeReference<String>() {
                });

        LOGGER.info("腾讯云人脸-出参 orderNo:{}, userId:{}, body:{}", orderNo, userId, responseEntity.getBody());

        JSONObject jsonObject = JSON.parseObject(responseEntity.getBody());
        String code = jsonObject.getString("code");
        if (!CODE_SUCCESS.equals(code)) {
            String msg = jsonObject.getString("msg");
            LOGGER.warn("腾讯云人脸-请求失败 orderNo:{}, userId:{}, code:{}, message:{}", orderNo, userId, code, msg);
            throw new BusinessException(ExceptionCodes.EXT_TENCEN_CLOUD_ERR, "(" + code + ")" + msg);
        }
        String bizSeqNo = jsonObject.get("bizSeqNo").toString();
        String faceId = jsonObject.getJSONObject("result").get("h5faceId").toString();
        orderNo = jsonObject.getJSONObject("result").get("orderNo").toString();
        String optimalDomain = jsonObject.getJSONObject("result").get("optimalDomain").toString();

        TencentFaceIdVO faceIdVO = TencentFaceIdVO.builder()
                .bizSeqNo(bizSeqNo)
                .webankAppId(wbAppId)
                .orderNo(orderNo)
                .h5faceId(faceId)
                .optimalDomain(optimalDomain)
                .build();
        LOGGER.info("腾讯云人脸-响应客户端参数 orderNo:{}, userId:{}, body:{}", orderNo, userId, JSON.toJSON(faceIdVO));
        return faceIdVO;
    }

    /**
     * 获取H5刷脸地址 https://cloud.tencent.com/document/product/1007/35884
     *
     * @param orderNo
     * @param h5faceId
     * @param userId
     * @return
     * @throws BusinessException
     */
    public static String getFaceH5LoginUrl(String orderNo, String h5faceId, String userId, String callback,
                                           String optimalDomain)
            throws BusinessException, UnsupportedEncodingException {
        String nonceStr = RandomStringUtils.random(32, true, true);
        String openApiTicket = getApiTicket(userId, "NONCE");
        String openApiSign = sign(Lists.newArrayList(wbAppId, orderNo, userId, VERSION, h5faceId, nonceStr), openApiTicket);
        StringBuilder builder = new StringBuilder();
        builder.append("https://" + optimalDomain + H5_FACE_LOGIN_URL);
        builder.append("?webankAppId=").append(wbAppId);
        builder.append("&version=").append(VERSION);
        builder.append("&nonce=").append(nonceStr);
        builder.append("&orderNo=").append(orderNo);
        builder.append("&h5faceId=").append(h5faceId);
        builder.append("&url=").append(URLEncoder.encode(callback, "UTF-8"));
        builder.append("&userId=").append(userId);
        builder.append("&sign=").append(openApiSign);
        builder.append("&from=").append("browser");
        builder.append("&redirectType=").append("1");

        return builder.toString();
    }

    /**
     * 获取PC刷脸地址 https://cloud.tencent.com/document/product/1007/35894
     *
     * @param orderNo
     * @param h5faceId
     * @param userId
     * @return
     * @throws BusinessException
     */
    public static String getFacePcLoginUrl(String orderNo, String h5faceId, String userId, String callback,
                                           String optimalDomain)
            throws BusinessException, UnsupportedEncodingException {
        String nonceStr = RandomStringUtils.random(32, true, true);
        String openApiTicket = getApiTicket(userId, "NONCE");
        String openApiSign = sign(Lists.newArrayList(wbAppId, orderNo, userId, VERSION, h5faceId, nonceStr), openApiTicket);
        StringBuilder builder = new StringBuilder();
        builder.append("https://" + optimalDomain + PC_FACE_LOGIN_URL);
        builder.append("?webankAppId=").append(wbAppId);
        builder.append("&version=").append(VERSION);
        builder.append("&nonce=").append(nonceStr);
        builder.append("&orderNo=").append(orderNo);
        builder.append("&h5faceId=").append(h5faceId);
        builder.append("&url=").append(URLEncoder.encode(callback, "UTF-8"));
        builder.append("&userId=").append(userId);
        builder.append("&sign=").append(openApiSign);
        builder.append("&from=").append("browser");
        builder.append("&redirectType=").append("1");

        return builder.toString();
    }


    @Builder
    @Data
    public static class FaceSyncVO {

        private Boolean result;
        private String name;
        private String idNo;
    }
}
